package com.youngjun.common.security.autoconfigure;

import com.youngjun.common.security.access.CustomAccessDecisionManager;
import com.youngjun.common.security.access.CustomFilterInvocationSecurityMetadataSource;
import com.youngjun.common.security.filter.*;
import com.youngjun.common.security.service.*;
import com.youngjun.common.security.service.impl.*;
import com.youngjun.common.util.PropUtils;
import com.youngjun.common.util.JwtTokenUtils;
import com.youngjun.common.util.SpringUtils;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.DependsOn;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.ObjectPostProcessor;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.annotation.web.configurers.ExpressionUrlAuthorizationConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.WebAttributes;
import org.springframework.security.web.access.intercept.FilterSecurityInterceptor;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.IOException;
import java.security.Security;

import static org.springframework.boot.autoconfigure.security.SecurityProperties.DEFAULT_FILTER_ORDER;

/**
 * @author admin
 */
@DependsOn(value = "springUtils")
@EnableWebSecurity
public class WebSecurityAutoConfiguration extends WebSecurityConfigurerAdapter {

    private final static String ACCESS_CONTROL_EXPOSE_HEADERS = "Access-Control-Expose-headers";

    private final static String JWT_AUTH_PROVIDE = "jwt";

    private final static String DEFAULT_AUTH_PROVIDE = "session";

    @Value("${security.x509.enabled:false}")
    Boolean x509Enabled;

    @Value("${security.strict.enabled:false}")
    Boolean strictEnabled;

    @Value("${security.auth.provider:jwt}")
    String authProvider;

    @Value("${security.jwt.header}")
    String jwtHeader;

    @Value("${security.jwt.token-head}")
    String jwtTokenHead;

    static {
        Security.addProvider(new BouncyCastleProvider());
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(SpringUtils.getBean(UserDetailsService.class));
        super.configure(auth);
    }

    @Override
    public void configure(WebSecurity web) throws Exception {
        super.configure(web);
    }

    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    @Bean
    PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        if(!JWT_AUTH_PROVIDE.equals(authProvider)){
            authProvider=DEFAULT_AUTH_PROVIDE;
        }
        ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry urlRegistry = http.authorizeRequests();
        // first denyAll;
        urlRegistry.anyRequest().denyAll();
        urlRegistry.withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {
            @Override
            public <O extends FilterSecurityInterceptor> O postProcess(O object) {
                CustomFilterInvocationSecurityMetadataSource customFilterInvocationSecurityMetadataSource =
                        new CustomFilterInvocationSecurityMetadataSource(SpringUtils.getBean(RequestMapService.class), strictEnabled);
                object.setSecurityMetadataSource(customFilterInvocationSecurityMetadataSource);
                object.setAccessDecisionManager(new CustomAccessDecisionManager());
                return object;
            }
        });
        //已自定义策略
        http.csrf().disable();

        http.cors().disable();

        http.httpBasic().disable();

        http.headers().xssProtection().block(true);

        http.headers().httpStrictTransportSecurity().includeSubDomains(true);

        http.exceptionHandling().authenticationEntryPoint(this::authEntryPoint);
        http.exceptionHandling().accessDeniedHandler(this::accessDeniedHandler);

        if(x509Enabled){
            http.x509().x509AuthenticationFilter(SpringUtils.getBean(X509AuthenticationFilter.class));
        }

        http.addFilterBefore(authenticationFilter(), UsernamePasswordAuthenticationFilter.class);

        if(JWT_AUTH_PROVIDE.equals(authProvider)){
            http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
            http.addFilterBefore(jwtAuthenticationTokenFilter(), RequestBodyReaderAuthenticationFilter.class);
        }
        http.logout()
                .logoutUrl("/logout")
                .logoutSuccessHandler(this::logoutSuccessHandler);
    }

    /**
     * 登录成功的回调
     *
     * @param request
     * @param response
     * @param authentication
     * @throws IOException
     */
    private void loginSuccessHandler(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException {
        response.setStatus(HttpStatus.OK.value());
        response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
        if(JWT_AUTH_PROVIDE.equals(authProvider)){
            JwtTokenUtils jwtTokenUtil = SpringUtils.getBean(JwtTokenUtils.class);
            UserDetails userDetails = (UserDetails) authentication.getPrincipal();
            String token = jwtTokenUtil.generateToken(userDetails.getUsername());
            response.setHeader(jwtHeader, jwtTokenHead+token);
            response.setHeader(ACCESS_CONTROL_EXPOSE_HEADERS, jwtHeader);
        }else{
            HttpSession session = request.getSession(false);
            if (session != null) {
                session.removeAttribute(WebAttributes.AUTHENTICATION_EXCEPTION);
            }
        }
        SpringUtils.getBean(AuthCallbackService.class).loginSuccess(request,response,authentication);
    }

    /**
     * 登录失败的回调
     *
     * @param request
     * @param response
     * @param e
     * @throws IOException
     */
    private void loginFailureHandler(
            HttpServletRequest request,
            HttpServletResponse response,
            AuthenticationException e) throws IOException {
        response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
        response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
        SpringUtils.getBean(AuthCallbackService.class).loginFailure(request,response,e);
    }

    /**
     * 退出登录的回调
     *
     * @param request
     * @param response
     * @param authentication
     * @throws IOException
     */
    private void logoutSuccessHandler(
            HttpServletRequest request,
            HttpServletResponse response,
            Authentication authentication) throws IOException {
        response.setStatus(HttpStatus.OK.value());
        response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
        SpringUtils.getBean(AuthCallbackService.class).logoutSuccess(request,response,authentication);
    }

    /**
     * 未登录时访问需要权限的接口时的回调
     *
     * @param request
     * @param response
     * @param e
     */
    private void authEntryPoint(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException {
        response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
        response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
        SpringUtils.getBean(AuthCallbackService.class).authEntryPoint(request,response,e);
    }

    /**
     * 权限不足的回调
     *
     * @param request
     * @param response
     * @param e
     */
    private void accessDeniedHandler(HttpServletRequest request, HttpServletResponse response, AccessDeniedException e) throws IOException {
        response.sendError(HttpServletResponse.SC_FORBIDDEN);
        response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
        SpringUtils.getBean(AuthCallbackService.class).accessDenied(request,response,e);
    }

    /**
     * registration XTokenFilter
     * @return
     */
    @Bean
    @ConditionalOnProperty(prefix ="security.x-token",name = "enabled",havingValue = "true")
    public FilterRegistrationBean<XTokenFilter> xTokenFilter() {
        FilterRegistrationBean<XTokenFilter> registrationBean = new FilterRegistrationBean<>();
        registrationBean.setOrder(DEFAULT_FILTER_ORDER - 100);
        registrationBean.addUrlPatterns("/*");
        registrationBean.setFilter(new XTokenFilter());
        return registrationBean;
    }

    /**
     * registration requestDataFilter
     * @return
     */
    @Bean
    public FilterRegistrationBean<RequestFilter> requestDataFilter() {
        FilterRegistrationBean<RequestFilter> registrationBean = new FilterRegistrationBean<>();
        registrationBean.setOrder(DEFAULT_FILTER_ORDER - 99);
        registrationBean.addUrlPatterns("/*");
        registrationBean.setFilter(new RequestFilter());
        return registrationBean;
    }

    @Bean
    @ConditionalOnMissingBean(CaptchaService.class)
    public CaptchaService captchaService(){
        return new DefaultCaptchaServiceImpl();
    }

    @Bean
    @ConditionalOnMissingBean(AuthCallbackService.class)
    public AuthCallbackService authCallbackService(){
        return new DefaultAuthCallbackServiceImpl();
    }

    @Bean("pkiService")
    @ConditionalOnMissingBean(PkiService.class)
    @ConditionalOnProperty(prefix ="pki",name = "enabled",havingValue = "true")
    public PkiService pkiService(){
        return new DefaultPkiServiceImpl();
    }

    @Bean("userDetailsService")
    @ConditionalOnMissingBean(UserDetailsService.class)
    public UserDetailsService customUserDetailsService(){
        return new DefaultUserDetailsServiceImpl();
    }

    @Bean
    @ConditionalOnMissingBean(RequestMapService.class)
    public RequestMapService requestMapService(){
        return new DefaultRequestMapServiceImpl();
    }

    @Bean
    @ConditionalOnProperty(prefix ="security.x509",name = "enabled",havingValue = "true")
    public X509AuthenticationFilter x509AuthenticationFilter() throws Exception {
        X509AuthenticationFilter x509AuthenticationFilter = new X509AuthenticationFilter();
        x509AuthenticationFilter.setX509CertificateValidator(x509Certificate -> SpringUtils.getBean(PkiService.class).verfyCert(x509Certificate));
        x509AuthenticationFilter.setAuthenticationManager(authenticationManagerBean());
        x509AuthenticationFilter.setAuthenticationFailureHandler(this::loginFailureHandler);
        return x509AuthenticationFilter;
    }

    @Bean
    @ConditionalOnExpression("'${security.auth.provider}'.equals('jwt')")
    @ConditionalOnProperty(name = {"header","token-head","secret","expiration","refresh-buffer"},prefix ="security.jwt")
    public JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter() {
        return new JwtAuthenticationTokenFilter();
    }

    @Bean
    @ConditionalOnExpression("'${security.auth.provider}'.equals('jwt')")
    @ConditionalOnProperty(name = {"header","token-head","secret","expiration","refresh-buffer"},prefix ="security.jwt")
    public JwtTokenUtils jwtTokenUtils() {
        String tokenHead = PropUtils.get("security.auth.provider.token-head");
        String secret = PropUtils.get("security.auth.provider.secret");
        Long expiration = PropUtils.get("security.auth.provider.expiration",Long.class);
        Long refreshBuffer = PropUtils.get("security.auth.provider.refresh-buffer",Long.class);
        return new JwtTokenUtils(secret,expiration,refreshBuffer,tokenHead);
    }

    /**
     * account & password -login
     *
     * @return
     * @throws Exception
     */
    @Bean
    public RequestBodyReaderAuthenticationFilter authenticationFilter() throws Exception {
        RequestBodyReaderAuthenticationFilter authenticationFilter = new RequestBodyReaderAuthenticationFilter();
        authenticationFilter.setPostOnly(true);
        authenticationFilter.setAuthenticationSuccessHandler(this::loginSuccessHandler);
        authenticationFilter.setAuthenticationFailureHandler(this::loginFailureHandler);
        authenticationFilter.setRequiresAuthenticationRequestMatcher(new AntPathRequestMatcher("/login", "POST"));
        authenticationFilter.setAuthenticationManager(authenticationManagerBean());
        return authenticationFilter;
    }
}
